package org.cerberus.platform.ograph;

import java.lang.ref.Reference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections.map.ReferenceMap;
import org.cerberus.platform.component.annotations.PAttribute;
import org.cerberus.platform.component.annotations.PComponent;

public class ObjectGraph implements IObjectGraph {

	private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
	
	private Object root;
	
	Map<Integer, Object> objectMap;
	Map<Class<?>, ComponentClass> classMap;
	Map<String, Integer> instanceCounter;
	
	@SuppressWarnings("unchecked")
	public ObjectGraph() {
		 objectMap = new ObjectGraphReferenceMap(ReferenceMap.HARD, ReferenceMap.WEAK, 10000, 0.75f, true);
		 classMap = new HashMap<Class<?>, ComponentClass>(500);
		 instanceCounter = new HashMap<String, Integer>(500);
	}
	
	@Override
	public Object getRoot() {
		return root;
	}

	@Override
	public ObjectGraphTransaction setRoot(Object o) {
		beginTransaction();
		if (this.root != null) {
			Object oldRoot = this.root;
			this.root = null;
			nodeRemoved(oldRoot);
		}
		if (o != null) {
			this.root = o;
			nodeAdded(o);
		}
		return returnTransaction();
	}
	
	private ObjectGraphTransaction transaction;
	
	private void beginTransaction() {
		transaction = new ObjectGraphTransaction();
	}
	
	private void classAdded(Class<?> clazz, boolean allowInvalid) {
		if (!classMap.containsKey(clazz)) {
			if (NativeClasses.isNativeClass(clazz)) {
				throw new ComponentClassException("Class "+clazz.getName()+ " is a system class and cannot be initialized.");
			}
			ComponentClass componentClass = defineClass(clazz);
			if (componentClass == null) {
				if (!allowInvalid) {
					throw new ComponentClassException("Class "+clazz.getName()+" could not be defined as PComponent.");
				} else {
					return;
				}
			}
			classMap.put(clazz, componentClass);
			transaction.addComponentClass(componentClass);
		}
	}
	
	private void classRemoved(Class<?> clazz) {
		ComponentClass componentClazz = classMap.get(clazz);
		if (componentClazz != null) {
			classMap.remove(clazz);
			transaction.removeComponentClass(componentClazz.getClassID());
		}
	}
	
	@Override
	public int getInternalID(Object o) {
		int hash = o.hashCode();
		if (objectMap.containsKey(hash)) {
			return hash;
		}
		return -1;
	}
	
	private void nodeAdded(Object o) {
		ComponentClass componentClass = classMap.get(o.getClass());
		if (componentClass == null) {
			classAdded(o.getClass(), false);
			componentClass = classMap.get(o.getClass());
		}
		int hash = o.hashCode();
		if (!objectMap.containsKey(hash)) {
			String classID = resolveClassID(o.getClass());
			objectMap.put(hash, o);
			if (!instanceCounter.containsKey(classID)) {
				instanceCounter.put(classID, 1);
			} else {
				instanceCounter.put(classID, instanceCounter.get(classID) + 1);
			}
			transaction.addObject(hash, o);
			walkNodeAdd(componentClass, o);
		}
	}
	
	private void walkNodeAdd(ComponentClass clazz, Object o) {
		ComponentClassAttribute[] attributes = clazz.getAttributes();
		for (int i = 0; i < attributes.length; i++) {
			ComponentClassAttribute componentClassAttribute = attributes[i];
			if (componentClassAttribute.isComponent()) {
				Object value = null;
				try {
					value = componentClassAttribute.getValue(o);
				} catch (ComponentClassException e) {
					throw e;
				} catch (Throwable e) {
					handleUserException(e);
				}
				if (value != null) {
					nodeAdded(value);
				}
			}
		}
	}
	
	
	private void walkNodeRemove(ComponentClass clazz, Object o) {
		ComponentClassAttribute[] attributes = clazz.getAttributes();
		for (int i = 0; i < attributes.length; i++) {
			ComponentClassAttribute componentClassAttribute = attributes[i];
			if (componentClassAttribute.isComponent()) {
				Object value = null;
				try {
					value = componentClassAttribute.getValue(o);
				} catch (ComponentClassException e) {
					throw e;
				} catch (Throwable e) {
					handleUserException(e);
				}
				if (value != null) {
					nodeRemoved(value);
				}
			}
		}
	}
	
	private void handleUserException(Throwable t) {
		t.printStackTrace();
		//TODO better handling
	}
	
	private void nodeRemoved(Object o) {
		if (!classMap.containsKey(o.getClass())) {
			// unknown in any way
			return;
		}
		int hash = o.hashCode();
		if (objectMap.containsKey(hash)) {
			String classID = resolveClassID(o.getClass());
			objectMap.remove(hash);
			int instanceCount = 0;
			if (instanceCounter.containsKey(classID)) {
				instanceCount = instanceCounter.get(classID) - 1;
				instanceCounter.put(classID, instanceCount);
			}
			transaction.removeObject(hash);
			
			walkNodeRemove(classMap.get(o.getClass()), o);
			
			if (instanceCount <= 0) {
				// instance counter reached zero or below
				// -> class can be removed
				classRemoved(o.getClass());
				instanceCounter.remove(classID);
			}
		}
	}
	
	private ObjectGraphTransaction returnTransaction() {
		Set<Integer> purgedValues = ((ObjectGraphReferenceMap)objectMap).purgeManually();
		if (purgedValues != null && purgedValues.size() > 0) {
			transaction.removeObject(purgedValues);
		}
		
		ObjectGraphTransaction returnTransaction = transaction;
		this.transaction = null;
		return returnTransaction;
	}
	
	@Override
	public void initializeClass(Class<?> clazz) {
		if (!classMap.containsKey(clazz)) {
			if (NativeClasses.isNativeClass(clazz)) {
				throw new ComponentClassException("Class "+clazz.getName()+ " is a system class and cannot be initialized.");
			}
			classMap.put(clazz, defineClass(clazz));
		}
	}
	
	@Override
	public ObjectGraphTransaction notifyChange(Object o, String attributeName) {
		beginTransaction();
		return returnTransaction();
	}
	
	@Override
	public ComponentClass getComponentClass(Class<?> clazz) {
		return classMap.get(clazz);
	}
	
	
	// component class definition area
	
	private ComponentClass defineClass(Class<? extends Object> objectClass) {
		PComponent classAnnotation = objectClass.getAnnotation(PComponent.class);
		if (classAnnotation == null) {
			return null;
		}
		String classID = classAnnotation.classID();
		boolean visible = classAnnotation.defaultVisible();
		boolean defaultModal = classAnnotation.modal();
		Class<? extends Object> superClass = objectClass.getSuperclass();
		ComponentClass superClassClass = null;
		
		//TODO warning if hashcode() is overwritten
		
		if (superClass != null && superClass != Object.class) {
			if (classMap.containsKey(superClass)) {
				superClassClass = classMap.get(superClass);
			} else {
				classAdded(superClass, true);
			}
		}
		ComponentClass clazz = new ComponentClass(superClassClass, 
				objectClass.getName(), classID, visible, defaultModal);
		
		clazz.defineAttributes(defineAttributes(objectClass));
		
		return clazz;
	}
	
	private ComponentClassAttribute[] defineAttributes(Class<?> clazz) {
		List<ComponentClassAttribute> list = new ArrayList<ComponentClassAttribute>();
		Field[] fields = clazz.getDeclaredFields();
		for (int i = 0; i < fields.length; i++) {
			Field field = fields[i];
			PAttribute attribute = field.getAnnotation(PAttribute.class);
			if (attribute != null) {
				String annotationName = attribute.name();
				if (annotationName.equals("")) {
					annotationName = field.getName();
				}
				Class<?> fieldType = field.getType();
				ComponentClassAttribute classAttribute = new ComponentClassAttribute(
						annotationName, 
						fieldType,
						resolveClassID(field.getType()),
						resolveGetter(clazz, field.getName(), fieldType),
						resolveSetter(clazz, field.getName(), fieldType));
				list.add(classAttribute);
			}
		}
		return list.toArray(new ComponentClassAttribute[list.size()]);
	}
	
	private String resolveClassID(Class<?> clazz) {
		if (NativeClasses.isNativeClass(clazz)) {
			return NativeClasses.getNativeClassID(clazz);
		}
		ComponentClass componentClass = classMap.get(clazz);
		if (componentClass == null) {
			return "<Any>";
		}
		return componentClass.getClassID();
	}
	
	private Method resolveGetter(Class<? extends Object> clazz, String fieldName, Class<?> attributeType) {
		Method foundMethod = null;
		String methodSuffix = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1, fieldName.length());
		String methodName = "get" + methodSuffix;
		try {
			foundMethod = clazz.getMethod(methodName, EMPTY_CLASS_ARRAY);
		} catch (Exception e) {
		}
		if (attributeType == boolean.class || attributeType == Boolean.class) {
			String booleanMethodName = "is" + methodSuffix;
			try {
				foundMethod = clazz.getMethod(booleanMethodName, EMPTY_CLASS_ARRAY);
			} catch (Exception e) {
			}
		}
		
		if (foundMethod == null) {
			throw new ComponentClassException("Could find a valid getter for attribute field "+fieldName+" in class "+clazz.getName()+" with returning type "+attributeType);
		}
		if (!foundMethod.getReturnType().equals(attributeType)) {
			throw new ComponentClassException("Found getter for attribute field "+fieldName+" in class "+clazz.getName()+" does not return the same type as the attribute definition "+attributeType.getName());
		}
		return foundMethod;
	}
	
	private Method resolveSetter(Class<? extends Object> clazz, String fieldName, Class<?> attributeType) {
		String methodSuffix = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1, fieldName.length());
		try {
			return clazz.getMethod("set"+methodSuffix, new Class<?>[]{attributeType});
		} catch (Exception e) {
		}
		return null;
	}
	
	
	
	
	
	
	static class ObjectGraphReferenceMap extends ReferenceMap {
		
		private static final long serialVersionUID = 1L;
		
		private Set<Integer> purgedValues = new HashSet<Integer>();
		
		public ObjectGraphReferenceMap(int keyType, int valueType,
				int capacity, float loadFactor, boolean purgeValues) {
			super(keyType, valueType, capacity, loadFactor, purgeValues);
		}

		@Override
		public void purge() {
			super.purge();
		}
		public Set<Integer> purgeManually() {
			super.purge();
			super.purge();
			
			if (purgedValues.size() > 0) {
				Set<Integer> purged = purgedValues;
				purgedValues = new HashSet<Integer>();
				return purged;
			}
			return purgedValues;
		}
		
	    protected void purge(Reference ref) {
	        // The hashCode of the reference is the hashCode of the
	        // mapping key, even if the reference refers to the 
	        // mapping value...
	    	purgedValues.add(ref.hashCode());
	        super.purge(ref);
	    }
		
		@Override
		protected void purgeBeforeRead() {
		}
		
		@Override
		protected void purgeBeforeWrite() {
		}
		
	}
}
